/**
 ** This file is part of Klistret. Klistret is free software: you can
 ** redistribute it and/or modify it under the terms of the GNU General
 ** Public License as published by the Free Software Foundation, either
 ** version 3 of the License, or (at your option) any later version.

 ** Klistret is distributed in the hope that it will be useful, but
 ** WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 ** General Public License for more details. You should have received a
 ** copy of the GNU General Public License along with Klistret. If not,
 ** see <http://www.gnu.org/licenses/>
 */

package com.klistret.cmdb.utility.hibernate;

import java.io.Serializable;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Calendar;
import java.util.TimeZone;

import org.hibernate.HibernateException;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.usertype.UserType;
import org.hibernate.usertype.UserVersionType;

/**
 * UserType for non-default TimeZone. Hibernate's built-in date, time and
 * timestamp types assume that dates in the database are in Java's default time
 * zone, implicitly. If this assumption is false (and you can't make it true by
 * calling java.util.TimeZone.setDefault), you can configure Hibernate to map to
 * a UserType that does something else....
 * 
 * This code is taken from {@link http ://www.hibernate.org/100.html}. However,
 * there are comments there which apply to us, namely that it would be useful to
 * treat the activeOn property as a version field which is maintained by the
 * database and it is not clear how to get that to work. Also the thread
 * {@link http ://forum.hibernate.org/viewtopic.php?t=980279} suggests handling
 * this conversion in the POJO instead. However, at least one link (reference?)
 * I found indicated a problem when the front-end and the middle layer lived in
 * different time zones. I'm not sure that applies to use since we don't allow
 * the front end to set timestamps however, if London starts handling releases,
 * there may be a problem displaying times in an easily understood form for the
 * users.
 */
public abstract class HibernateUTC implements UserVersionType, UserType {

	/** the SQL type this type manages */
	protected static int[] SQL_TYPES_UTC = { Types.TIMESTAMP };

	/**
	 * @see net.sf.hibernate.UserType#sqlTypes()
	 */
	public int[] sqlTypes() {
		return SQL_TYPES_UTC;
	}

	/**
	 * @see net.sf.hibernate.UserType#equals(java.lang.Object, java.lang.Object)
	 */
	public boolean equals(Object x, Object y) {
		return (x == null) ? (y == null) : x.equals(y);
	}

	/**
	 * @see net.sf.hibernate.UserType#isMutable()
	 */
	public boolean isMutable() {
		return true;
	}

	/**
	 * @see net.sf.hibernate.UserType#returnedClass()
	 */
	public Class<?> returnedClass() {
		return objectClass;
	}

	/**
	 * The class of objects returned by <code>nullSafeGet</code>. Currently,
	 * returned objects are derived from this class, not exactly this class.
	 */
	protected Class<?> objectClass = Date.class;

	/**
	 * Get a hashcode for the instance, consistent with persistence "equality"
	 */
	public int hashCode(Object x) throws HibernateException {
		return x.hashCode();
	}

	/**
	 * Transform the object into its cacheable representation. At the very least
	 * this method should perform a deep copy if the type is mutable. That may
	 * not be enough for some implementations, however; for example,
	 * associations must be cached as identifier values. (optional operation)
	 * 
	 * @param value
	 *            the object to be cached
	 * @return a cachable representation of the object
	 * @throws HibernateException
	 */
	public Serializable disassemble(Object value) throws HibernateException {
		return (Serializable) deepCopy(value);
	}

	/**
	 * Reconstruct an object from the cacheable representation. At the very
	 * least this method should perform a deep copy if the type is mutable.
	 * (optional operation)
	 * 
	 * @param cached
	 *            the object to be cached
	 * @param owner
	 *            the owner of the cached object
	 * @return a reconstructed object from the cachable representation
	 * @throws HibernateException
	 */
	public Object assemble(Serializable cached, Object owner)
			throws HibernateException {
		return deepCopy(cached);
	}

	/**
	 * During merge, replace the existing (target) value in the entity we are
	 * merging to with a new (original) value from the detached entity we are
	 * merging. For immutable objects, or null values, it is safe to simply
	 * return the first parameter. For mutable objects, it is safe to return a
	 * copy of the first parameter. For objects with component values, it might
	 * make sense to recursively replace component values.
	 * 
	 * @param original
	 *            the value from the detached entity being merged
	 * @param target
	 *            the value in the managed entity
	 * @return the value to be merged
	 */
	public Object replace(Object original, Object target, Object owner)
			throws HibernateException {
		return deepCopy(original);
	}

	public Object seed(SessionImplementor si) {
		return null;
	}

	public Object next(Object current, SessionImplementor si) {
		return null;
	}

	/**
	 * Like a Hibernate date, but using the UTC TimeZone (not the default
	 * TimeZone).
	 */
	public static class DateType extends HibernateUTC {
		protected static int[] SQL_TYPES_DATE = { Types.DATE };

		/**
		 * @see net.sf.hibernate.UserType#sqlTypes()
		 */
		public int[] sqlTypes() {
			return SQL_TYPES_DATE;
		}

		/**
		 * @see net.sf.hibernate.UserType#deepCopy(java.lang.Object)
		 */
		public Object deepCopy(Object value) {
			return (value == null) ? null : new java.sql.Date(((Date) value)
					.getTime());

		}

		/**
		 * @see net.sf.hibernate.UserType#nullSafeGet(java.sql.ResultSet,
		 *      java.lang.String[], java.lang.Object)
		 */
		public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
				throws SQLException {
			Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
			return rs.getDate(names[0], utcCalendar);
		}

		/**
		 * @see net.sf.hibernate.UserType#nullSafeSet(java.sql.PreparedStatement,
		 *      java.lang.Object, int)
		 */
		public void nullSafeSet(PreparedStatement st, Object value, int index)
				throws SQLException {
			if (!(value instanceof java.sql.Date))
				value = deepCopy(value);
			Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
			st.setDate(index, (java.sql.Date) value, utcCalendar);
		}

		public int compare(Object x, Object y) {
			if (x == null && y == null)
				return 0;
			else if (x == null)
				return 1;
			else if (y == null)
				return -1;
			else {
				java.sql.Date c1 = (java.sql.Date) x;
				java.sql.Date c2 = (java.sql.Date) y;
				return compare(c1, c2);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
		 */
		public int hashCode(Object x) throws HibernateException {
			return ((java.sql.Date) x).hashCode();
		}

		public Object seed(SessionImplementor si) {
			return new java.sql.Date(System.currentTimeMillis());
		}

		public Object next(Object current, SessionImplementor si) {
			return new java.sql.Date(System.currentTimeMillis());
		}
	}

	/**
	 * Like a Hibernate time, but using the UTC TimeZone (not the default
	 * TimeZone).
	 */
	public static class TimeType extends HibernateUTC {

		protected static int[] SQL_TYPES_TIME = { Types.TIME };

		/**
		 * @see net.sf.hibernate.UserType#sqlTypes()
		 */
		public int[] sqlTypes() {
			return SQL_TYPES_TIME;
		}

		/**
		 * @see net.sf.hibernate.UserType#deepCopy(java.lang.Object)
		 */
		public Object deepCopy(Object value) {
			return (value == null) ? null : new java.sql.Time(((Date) value)
					.getTime());
		}

		/**
		 * @see net.sf.hibernate.UserType#nullSafeGet(java.sql.ResultSet,
		 *      java.lang.String[], java.lang.Object)
		 */
		public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
				throws SQLException {
			Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
			return rs.getTime(names[0], utcCalendar);
		}

		/**
		 * @see net.sf.hibernate.UserType#nullSafeSet(java.sql.PreparedStatement,
		 *      java.lang.Object, int)
		 */
		public void nullSafeSet(PreparedStatement st, Object value, int index)
				throws SQLException {
			if (!(value instanceof java.sql.Time))
				value = deepCopy(value);
			Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
			st.setTime(index, (java.sql.Time) value, utcCalendar);
		}

		public int compare(Object x, Object y) {
			if (x == null && y == null)
				return 0;
			else if (x == null)
				return 1;
			else if (y == null)
				return -1;
			else {
				java.sql.Time c1 = (java.sql.Time) x;
				java.sql.Time c2 = (java.sql.Time) y;
				return compare(c1, c2);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
		 */
		public int hashCode(Object x) throws HibernateException {
			return ((java.sql.Time) x).hashCode();
		}

		public Object seed(SessionImplementor si) {
			return new java.sql.Time(System.currentTimeMillis());
		}

		public Object next(Object current, SessionImplementor si) {
			return new java.sql.Time(System.currentTimeMillis());
		}

	}

	/**
	 * Like a Hibernate timestamp, but using the UTC TimeZone (not the default
	 * TimeZone).
	 */
	public static class TimestampType extends HibernateUTC {

		/**
		 * @see net.sf.hibernate.UserType#deepCopy(java.lang.Object)
		 */
		public Object deepCopy(Object value) {
			if (value == null)
				return null;
			java.sql.Timestamp ots = (java.sql.Timestamp) value;
			java.sql.Timestamp ts = new java.sql.Timestamp(ots.getTime());
			ts.setNanos(ots.getNanos());
			return ts;
		}

		/**
		 * @see net.sf.hibernate.UserType#nullSafeGet(java.sql.ResultSet,
		 *      java.lang.String[], java.lang.Object)
		 */
		public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
				throws SQLException {
			Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
			return rs.getTimestamp(names[0], utcCalendar);
		}

		/**
		 * @see net.sf.hibernate.UserType#nullSafeSet(java.sql.PreparedStatement,
		 *      java.lang.Object, int)
		 */

		public void nullSafeSet(PreparedStatement st, Object value, int index)
				throws SQLException {
			if (!(value instanceof java.sql.Timestamp))
				value = deepCopy(value);
			Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
			st.setTimestamp(index, (java.sql.Timestamp) value, utcCalendar);
		}

		public int compare(Object x, Object y) {
			if (x == null && y == null)
				return 0;
			else if (x == null)
				return 1;
			else if (y == null)
				return -1;
			else {
				Timestamp c1 = (Timestamp) x;
				Timestamp c2 = (Timestamp) y;
				return compare(c1, c2);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
		 */
		public int hashCode(Object x) throws HibernateException {
			return ((Timestamp) x).hashCode();
		}

		public Object seed(SessionImplementor si) {
			return new Timestamp(System.currentTimeMillis());
			// return new Timestamp(si.getTimestamp());
		}

		public Object next(Object current, SessionImplementor si) {
			return new Timestamp(System.currentTimeMillis());
			// return new Timestamp(si.getTimestamp());
		}

		public Class<?> getReturnedClass() {
			return java.sql.Timestamp.class;
		}
	}

	public static class CalendarType extends HibernateUTC {

		public Class<?> getReturnedClass() {
			return Calendar.class;
		}

		/**
		 * @see net.sf.hibernate.UserType#deepCopy(java.lang.Object)
		 */
		public Object deepCopy(Object value) {
			if (value == null) {
				return null;
			}
			Calendar c = (Calendar) UTC_CALENDAR.clone();
			c.setTimeInMillis(((Calendar) value).getTimeInMillis());
			return c;
		}

		/**
		 * @see net.sf.hibernate.UserType#nullSafeGet(java.sql.ResultSet,
		 *      java.lang.String[], java.lang.Object)
		 */
		public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
				throws SQLException {
			Calendar cal = (Calendar) UTC_CALENDAR.clone();
			Timestamp ts = rs.getTimestamp(names[0], cal);
			if (ts == null || rs.wasNull()) {
				return null;
			}
			if (Environment.jvmHasTimestampBug()) {
				cal.setTime(new Date(ts.getTime() + ts.getNanos() / 1000000));
			} else {
				cal.setTime(ts);
			}
			return cal;

		}

		/**
		 * @see net.sf.hibernate.UserType#nullSafeSet(java.sql.PreparedStatement,
		 *      java.lang.Object, int)
		 */

		public void nullSafeSet(PreparedStatement st, Object value, int index)
				throws SQLException {
			if (value == null) {
				st.setNull(index, Types.TIMESTAMP);
			} else {
				Timestamp t = new Timestamp(((Calendar) value)
						.getTimeInMillis());
				Calendar utcCalendar = (Calendar) UTC_CALENDAR.clone();
				st.setTimestamp(index, t, utcCalendar);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.hibernate.usertype.UserType#equals(java.lang.Object,
		 *      java.lang.Object)
		 */
		public boolean equals(Object x, Object y) {
			if (x == y)
				return true;
			if (x == null || y == null)
				return false;

			Calendar calendar1 = (Calendar) x;
			Calendar calendar2 = (Calendar) y;

			return calendar1.getTimeInMillis() == calendar2.getTimeInMillis();
		}

		public int compare(Object x, Object y) {
			if (x == null && y == null)
				return 0;
			else if (x == null)
				return 1;
			else if (y == null)
				return -1;
			else {
				Calendar c1 = (Calendar) x;
				Calendar c2 = (Calendar) y;
				return compare(c1, c2);
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.hibernate.usertype.UserType#hashCode(java.lang.Object)
		 */
		public int hashCode(Object x) throws HibernateException {
			return ((Calendar) x).hashCode();
		}

		public Object seed(SessionImplementor si) {
			Calendar cal = (Calendar) UTC_CALENDAR.clone();
			cal.setTimeInMillis(System.currentTimeMillis());
			return cal;
		}

		public Object next(Object current, SessionImplementor si) {
			Calendar cal = (Calendar) UTC_CALENDAR.clone();
			cal.setTimeInMillis(System.currentTimeMillis());
			return cal;
		}

	}

	/**
	 * Note 071107: passing the static sUTCCalendar instance to the
	 * setTimestamp(), getTimestamp() calls above has concurrency issues, as
	 * some JDBC drivers do modify the supplied calendar instance. Consequence,
	 * the calendar is cloned before use.
	 */

	/** the Calendar to hold the UTC timezone */
	private static final TimeZone TZ_UTC;
	private static final Calendar UTC_CALENDAR;
	static {
		TZ_UTC = TimeZone.getTimeZone("UTC");
		UTC_CALENDAR = Calendar.getInstance(TZ_UTC);
	}

	public static Calendar getCurrentCalendar() {
		Calendar calendar = (Calendar) UTC_CALENDAR.clone();
		calendar.setTimeInMillis(System.currentTimeMillis());
		return calendar;
	}
}
